[6.x] Modernize garnish#19129
Draft
brianjhanson wants to merge 13 commits into
Draft
Conversation
…kage Introduce packages/craftcms-garnish: a modern, tree-shakeable TypeScript rewrite of the legacy jQuery-based Garnish UI library, alongside an opt-in compatibility layer so existing consumers keep working unchanged. - garnish-core: native ES-class Base, three event systems (instance EventEmitter, class-level bus, namespaced DOM listener registry), custom events (activate/textchange/resize) without $.event.special, UiLayerManager/EscManager, focusable matcher, and the full utility surface. Zero jQuery in the modern entry (dist/index.js has no jQuery references). - Modal: vertical-slice PoC component (Velocity -> Web Animations API). - compat: opt-in ./compat entry restoring Garnish.Base.extend(), this.base(), window.Garnish, and jQuery-collection args; jQuery is an optional peer dependency of this entry only. - Tooling: tsdown ESM build, Vitest (happy-dom) with 121 passing tests, a Vite playground (npm run dev), TSDoc on public APIs, README + docs/. Draggable/resizable Modal (BaseDrag/DragMove) is deferred; see docs/00-migration-plan.md for the full migration plan and effort estimate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the legacy jQuery drag base into the modern package, jQuery-free, and use it to complete Modal's deferred draggable/resizable support. - src/drag/base-drag.ts: BaseDrag extends Base, modernized to Pointer Events (dropping the legacy mouse+touch shim), with a native requestAnimationFrame auto-scroll loop, a WeakMap drag registry, multi-touch pointer-id guarding, and the full subclass contract (addItems/removeItems/startDragging/drag/ stopDragging + onDragStart/onDrag/onDragStop hooks; beforeDragStart/dragStart/ drag/dragStop events). - src/drag-move.ts: replace the throwing PoC stub with the real DragMove. - src/utils/scroll.ts: shared axis-aware getScrollParent (animation.ts now consumes it); add getOuterWidth/Height reserved for the future Drag port. - src/modal.ts: remove the draggable/resizable throw blocks; draggable uses DragMove (container or dragHandleSelector), resizable uses a BaseDrag-driven resize handle with RTL-aware math; both torn down in destroy(). - Playground: draggable/header-handle/resizable Modal demos, standalone DragMove/BaseDrag boxes (incl. axis-locked), and an auto-scroll demo. - Docs: BaseDrag/DragMove API reference, updated Modal status, design + impl notes (docs/07, docs/08). 162 tests pass; the modern entry stays jQuery-free. Live drag/auto-scroll/ resize and touch behavior are validated via the playground (not unit-testable in happy-dom). Drag/DragDrop/DragSort remain the next drag-cluster modules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the next two drag-cluster modules onto the modern BaseDrag. - src/drag/drag.ts: Drag extends BaseDrag — native helper-clone creation (cloneNode(true), border-box sizing, input-name blanking), cursor lag-follow positioning, and Web Animations API return-to-source / fade-out of helpers (mirrors Modal._fade: tracked, cancelable, prefers-reduced-motion gated). Preserves the legacy startDragging hook ordering (onBeforeDragStart before helpers are built). - src/drag/drag-drop.ts: DragDrop extends Drag — drop-target resolution with native rect/hitTest hit detection and onDropTargetChange (fires on change only); $activeDropTarget is a raw HTMLElement. - src/utils/misc.ts: promote shared isPlainObject (base-drag.ts now imports it). - src/index.ts: Drag/DragDrop named exports + on the Garnish namespace. - Playground: "Drag with helpers (clones + return-to-source)" and "DragDrop — drop targets & hit detection" demos. - Docs: Drag/DragDrop API reference + status (only DragSort now pending in the drag cluster); design (09) + impl notes (10). 215 tests pass; modern entry stays jQuery-free. Live helper trailing, return animation, and drop hover are validated via the playground (not unit-testable in happy-dom). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DragMove.onDrag wrote the cursor's page coordinates straight into
style.left/top, which only works when the element's containing block is at
the page origin (e.g. a <body>-relative Modal). For an absolutely-positioned
element nested in a positioned ancestor, left/top are resolved against that
ancestor's padding box, so the element jumped by the container's page offset
and flew off-screen as soon as the drag started.
Convert the page-coordinate target into the element's containing-block
coordinates by subtracting its offsetParent origin (getOffset + clientLeft/Top
- scrollLeft/Top). Elements with no positioned ancestor (offsetParent null/
body/html) return {0,0}, so Modal and other body-relative draggers are
unchanged. Also fix the playground's raw-BaseDrag demo to position via the
cursor delta from a captured start (offset-parent-agnostic).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds `DragSort` (the sortable-list dragger) to `@craftcms/garnish`, completing the Phase-2 drag cluster (BaseDrag → DragMove → Drag → DragDrop → DragSort). `DragSort extends Drag` and ports the legacy `DragSort.js` faithfully, fully jQuery-free: - Live insertion feedback: the draggee block (+ optional `insertion` placeholder) is re-inserted into the DOM at the closest landing spot as the cursor moves. - `_getClosestItem` spatial hit-test (axis-aware x/y/Euclidean distance, outward walk with a monotonic-distance early-skip, `canInsertBefore`/`canInsertAfter` gating) backed by a per-drag midpoint cache (`_precalculateMidpoints`, only the moved item + neighbors recomputed per move, viewport filter for lists > 200). - `magnetStrength`, `moveTargetItemToFront`, axis options, and the `insertionPointChange` / `sortChange` / `dragStart` / `dragStop` events (legacy names preserved). - jQuery removed throughout: `.insertBefore/After` → `ChildNode.before/after`; `.offset()` → `getOffset`; `.index()` → manual sibling/`$items` index; `$().add()` DOM re-sort → `compareDocumentPosition`; `$.contains` → `Node.contains`; `$.data` midpoints → a `Map`. Also: named + `Garnish`-namespace exports; 32 happy-dom tests (247 total); a "DragSort — reorderable list" playground demo; design note (doc 11) + impl notes (doc 12); README and API reference updated (DragSort supported, drag cluster COMPLETE). Verified: check:types, test (247), build, playground:build, check:format all green; `dist/index.js` is jQuery-free (grep count 0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds `HUD` (Phase 3) — the anchored popover/bubble with smart 4-way positioning, a tip/arrow, scroll-follow, focus trapping, and UiLayerManager layer + Escape integration — as a jQuery-free `class HUD extends Base`. This was the last FieldLayoutDesigner overlay blocker. - src/hud.ts: faithful port of legacy HUD.js. jQuery removals mapped to the modern utils — `.scrollParent()`→getScrollParent, `.offset()`/getOffset, `.outerWidth/Height`→getOuterWidth/Height, `:focusable`→getFocusableElements/ getKeyboardFocusableElements, `$.data`→WeakMap, `Garnish.within`→within, RAF re-export, UiLayerManager via the registry. Legacy HUD has no Velocity, so show/hide stay as display toggles (no WAAPI conversion). Body content box measured via getBoundingClientRect (jQuery `.width()/.height()` parity, and mockable in happy-dom). - src/index.ts: named exports HUD + HUDSettings/HUDOrientation/HUDBodyContents, and HUD on the legacy-shaped Garnish namespace. - tests/hud.test.ts: 39 tests (settings/defaults, construction + param-shift, updateBody, show/hide/toggle + events, layer/Esc/shade, submit, focus, 4-way positioning with mocked rects, updateRecords, destroy). - playground: section 11 "HUD — anchored popover" (edge-anchored triggers + event log + HUD/tip CSS). - docs: 13-hud-design.md, 14-hud-impl-notes.md; README + 06-api-reference mark HUD supported. Gates: check:types, test (286, +39), build, playground:build, check:format all green; dist/index.js jQuery refs = 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A draggable Modal is position:fixed, so its offsetParent is null and its
left/top resolve against the viewport, not the page. containingBlockOrigin
treated a null offsetParent as the page origin {0,0}, so the first drag
frame wrote a page-Y target (which includes scrollY) into a viewport-relative
top — jumping the modal down by the page scroll offset.
Split the null-offsetParent branch: a position:fixed element's containing
block is the viewport anchored at the scroll offset, so its origin is
{scrollX, scrollY}. Absolute (no positioned ancestor) and detached elements
still resolve to {0,0}; the positioned-ancestor branch is unchanged. Adds a
regression test asserting a fixed target with scrollY set lands
viewport-relative (no jump).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port Garnish's largest remaining UI component (~1,008 LOC) to the modern,
jQuery-free TypeScript/ESM package, following the HUD/Modal overlay patterns.
`class DisclosureMenu extends Base<DisclosureMenuSettings>`:
- Resolves the menu panel from the trigger's aria-controls (or next sibling),
relocates it to <body>, and toggles via aria-expanded.
- Above/below + left/center/right anchored positioning (scroll/resize-aware).
- Full keyboard nav (arrows + Tab cycle), type-ahead search, focus management.
- UiLayerManager layer + Escape shortcut; outside-mousedown dismissal.
- Instant show; WAAPI fade-out on hide (reduced-motion aware) — Velocity removed.
- Item/group builders (addItem/addItems/addGroup/addHr/removeItem/...) the
~19 CP consumer sites rely on; per-item onActivate/callback selection.
- Optional withSearchInput live item filter.
jQuery removals: $(trigger)->getElement, :focusable->focusable matcher,
.scrollParent()->getScrollParent, .velocity('fadeOut')->WAAPI,
$.data('disclosureMenu'/'searchText')->WeakMaps, .find/closest/prevUntil/
nextUntil->native DOM. Craft.getUrl/t/initUiElements routed through an optional
global; $(el).formsubmit() omitted (sets the formsubmit class + data-action).
Exports DisclosureMenu (+ settings/item types) as named exports and on the
legacy-shaped Garnish namespace. Adds 49 happy-dom tests (suite: 336 passing),
a playground section 12 demo, design doc 15, impl notes 16, and README +
06-api-reference entries marking DisclosureMenu supported.
dist/index.js stays jQuery-free (grep -ciE "jquery|\$(" -> 0).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the single-page Vite playground (12 demo sections in one ~900-line main.ts) with Storybook, one story file per component. - Add .storybook config (framework @storybook/html-vite to match Garnish's imperative widgets; addons docs/a11y/themes at 10.4.1, mirroring @craftcms/cp). preview.css migrates the demo globals from playground/styles.css. - Add stories/ (top-level, mirroring tests/) with all 12 playground demos: modal (basic + draggable/resizable), focus (matcher + trap), compat (.extend/this.base), base-drag (standalone boxes + auto-scroll), drag (helpers + return/fade), drag-drop, drag-sort, hud, disclosure-menu (dropdown + filterable), utilities. Shared stories/_log.ts event-log helper (panel + drag-event wiring + layout) and stories/_helpers.ts modal builders. Stories import the real source from ../src; args/argTypes drive settings. - package.json: repoint "dev" to Storybook, add "storybook" + "build:storybook", remove "playground:build"; extend format globs to stories + .storybook; add Storybook devDeps at ^10.4.1. - tsconfig.json includes stories + .storybook; remove playground/ and the package-root vite.config.ts (it only served the playground). - README + new docs/17-storybook-notes.md document the Storybook workflow and how future component ports add a stories/<name>.stories.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hand-rolled in-canvas event-log panel with Storybook's built-in Actions panel (`storybook/actions`, part of core in 10.x — no extra addon). - stories/_log.ts: createEventLog() now wraps `action()`, memoizing one named action per tag so events group by tag in the Actions panel. Drop the panel/ clear/initialMessage API; storyLayout(main) just tags the demo container. - Update all story call sites: createEventLog(), storyLayout(main), drop the isError third arg. - preview.css: remove the .pg-log* panel styles; .pg-story is now a simple single-column demo container, not a two-column flex with a side panel. - Update docs/17-storybook-notes.md and README to describe the Actions panel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the ported FieldLayoutDesigner to native DOM + WeakMaps, keeping thin jQuery only where it hands off to Craft's still-jQuery widgets (Craft.Grid/Listbox/SlidePicker/SortableCheckboxSelect/Slideout, the .disclosureMenu() plugin, and form .serialize()). - All of FLD's own jQuery converted to native: createElement/querySelector, classList, dataset, textContent/innerHTML/value, native tree ops, getOffset/getOuterHeight, Web Animations API for fades. - .data() replaced with module-level WeakMaps in support.ts (fld-tab/fld-element/hud/cvd + drag midpoints) and dataset reads. - Garnish Drag $items/$draggee/helpers used as native arrays (wrappers removed); $insertion/$caboose are native too. - README updated to describe the native conversion and remaining seams. Gates: FLD typecheck clean, eslint clean. Full vite build is env-blocked in this worktree (no vendor/) and not run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rumors of my death are greatly exaggerated
I've taken a few runs at avoiding this, but nothing's felt quite right. So this is how I learned to stop worrying and love Garnish.
At the start of this endeavor it felt like a gargantuan task to bring Garnish into the modern age. That's all kind of changed now that we have agents that can do large swaths of the grunt work.
This PR represents a partial PoC but more likely the start of bringing Garnish into the modern age. Converting it to typescript, removing jQuery and adding better documentation via a Storybook.
While I don't think Garnish is perfect, it is pretty good, and has been consistently improved over time. I don't want to discard all those learnings from the start, but by splitting and modernizing Garnish we gain some flexibility in replacing the inner workings.
Still very much a work in progress, just opening to get the idea out there. I'm not sure I want this to live in a separate package but doing it this way made it easier to work through without messing up too much other stuff.